﻿/*******************************************************************************
Copyright (c) 2011-2013, PKStudio
Copyright (c) 2011,      Zynga Inc.
Copyright (c) 2008-2010, Ricardo Quesada

http://pkstudio.org

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*******************************************************************************/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Xml;

namespace PKEngine
{
	/// <summary>
	/// TmxLayerInfo contains the information about the layers.
	/// </summary>
	public class PKTmxLayerInfo
	{
		#region Public Properties
		public Dictionary<string, string> Properties { get; private set; }
		public string Name { get; set; }
		public Size LayerSize { get; set; }
		public int[] Tiles { get; set; }
		public bool IsVisible { get; set; }
		public int Opacity { get; set; }
		public int MaxGId { get; set; }
		public int MinGId { get; set; }
		public Point Offset { get; set; }
		#endregion

		#region Initialization
		public PKTmxLayerInfo()
		{
			this.Properties = new Dictionary<string, string>();
			this.MaxGId = 0;
			this.MinGId = 100000;
		}
		#endregion
	}

	/// <summary>
	/// TMXTilesetInfo contains the information about the tilesets.
	/// </summary>
	public class PKTmxTilesetInfo
	{
		#region Public Properties
		public string Name { get; set; }
		public int FirstGId { get; set; }
		public Size TileSize { get; set; }
		public int Spacing { get; set; }
		public int Margin { get; set; }
		public string SourceImage { get; set; }
		public Size ImageSize { get; set; }
		#endregion

		#region Public Methods
		public Rectangle RectForGId(int gid)
		{
			gid = gid - FirstGId;
			int xMax = ((ImageSize.Width - Margin * 2 + Spacing) / (TileSize.Width + Spacing));
			//int yMax = ((ImageSize.Height - Margin * 2 + Spacing) / (TileSize.Height + Spacing));
			int x = (gid % xMax) * (TileSize.Width + Spacing) + Margin;
			int y = (gid / xMax) * (TileSize.Height + Spacing) + Margin;
			return new Rectangle(new Point(x, y), TileSize);
		}
		#endregion
	}

	/// <summary>
	/// TMXMapInfo contains the information about the maps.
	/// </summary>
	public class PKTmxMapInfo
	{
		#region Public Properties
		public Dictionary<string, string> Properties { get; private set; }
		public Dictionary<int, Dictionary<string, string>> TileProperties { get; private set; }
		public Size MapSize { get; set; }
		public Size TileSize { get; set; }
		public List<PKTmxLayerInfo> Layers { get; private set; }
		public List<PKTmxTilesetInfo> Tilesets { get; private set; }
		public List<PKTmxObjectGroup> ObjectGroups { get; private set; }
		public string FileName { get; set; }
		public string Resources { get; set; }
		#endregion

		#region Initialization
		private void Initialze(string tmxFile, string resourcePath)
		{
			this.Properties = new Dictionary<string, string>();
			this.TileProperties = new Dictionary<int, Dictionary<string, string>>();
			this.Layers = new List<PKTmxLayerInfo>();
			this.Tilesets = new List<PKTmxTilesetInfo>();
			this.ObjectGroups = new List<PKTmxObjectGroup>();
			this.FileName = tmxFile;
			this.Resources = resourcePath;
		}

		public PKTmxMapInfo(string tmxFile)
		{
			this.Initialze(tmxFile, null);

			XmlDocument document = new XmlDocument();
			document.XmlResolver = null; // To avoid System.Net exceptions caused by dtd online.
			document.Load(tmxFile);

			this.ParseMap(document.LastChild as XmlElement);
		}

		public PKTmxMapInfo(string tmxText, string resourcePath)
		{
			this.Initialze(null, resourcePath);

			XmlDocument document = new XmlDocument();
			document.XmlResolver = null;
			document.LoadXml(tmxText);

			this.ParseMap(document.LastChild as XmlElement);
		}
		#endregion

		#region Private Methods
		private void ParseMap(XmlElement element)
		{
			string version = element.GetAttribute("version");
			if (version != "1.0")
			{
				throw new NotSupportedException("PKTmxMapInfo.ParseMap: Unsupported TMX version: " + version);
			}

			string orientation = element.GetAttribute("orientation");
			if (orientation != "orthogonal")
			{
				throw new NotSupportedException("PKTmxMapInfo.ParseMap: Unsupported TMX orthogonal: " + orientation);
			}

			Size s = Size.Empty;
			s.Width = int.Parse(element.GetAttribute("width"));
			s.Height = int.Parse(element.GetAttribute("height"));
			MapSize = s;

			s.Width = int.Parse(element.GetAttribute("tilewidth"));
			s.Height = int.Parse(element.GetAttribute("tileheight"));
			TileSize = s;

			foreach (XmlElement subElement in element.ChildNodes)
			{
				if (subElement.Name == "tileset")
				{
					this.ParseTileset(subElement, 0);
				}
				else if (subElement.Name == "layer")
				{
					this.ParseLayer(subElement);
				}
				else if (subElement.Name == "objectgroup")
				{
					this.ParseObjectGroup(subElement);
				}
				else if (subElement.Name == "properties")
				{
					ParseProperties(subElement, Properties);
				}
			}
		}

		private void ParseTileset(XmlElement element, int currentFirstGId)
		{
			string externalTilesetFilename = element.GetAttribute("source");
			if (!string.IsNullOrEmpty(externalTilesetFilename))
			{
				externalTilesetFilename = this.GetFileFullPath(externalTilesetFilename);

				int firstGId = int.Parse(element.GetAttribute("firstgid"));

				XmlDocument document = new XmlDocument();
				document.XmlResolver = null;
				document.Load(externalTilesetFilename);

				this.ParseTileset(document.LastChild as XmlElement, firstGId);
			}
			else
			{
				PKTmxTilesetInfo tileset = new PKTmxTilesetInfo();
				tileset.Name = element.GetAttribute("name");
				if (currentFirstGId == 0)
				{
					tileset.FirstGId = int.Parse(element.GetAttribute("firstgid"));
				}
				else
				{
					tileset.FirstGId = currentFirstGId;
				}
				int spacing;
				if (int.TryParse(element.GetAttribute("spacing"), out spacing))
				{
					tileset.Spacing = spacing;
				}
				int margin;
				if (int.TryParse(element.GetAttribute("margin"), out margin))
				{
					tileset.Margin = margin;
				}

				Size s = Size.Empty;
				s.Width = int.Parse(element.GetAttribute("tilewidth"));
				s.Height = int.Parse(element.GetAttribute("tileheight"));
				tileset.TileSize = s;

				foreach (XmlElement subElement in element.ChildNodes)
				{
					if (subElement.Name == "tile")
					{
						this.ParseTile(subElement, tileset);
					}
					else if (subElement.Name == "image")
					{
						string sourceImage = subElement.GetAttribute("source");
						if (string.IsNullOrEmpty(sourceImage))
						{
							throw new NotSupportedException("PKTmxMapInfo.ParseTileset: Does not support tile without source image.");
						}
						else
						{
							tileset.SourceImage = this.GetFileFullPath(sourceImage);
						}
					}
				}

				Tilesets.Add(tileset);
			}
		}

		private void ParseTile(XmlElement element, PKTmxTilesetInfo parentTileset)
		{
			Dictionary<string, string> dict = new Dictionary<string, string>();
			int gid = parentTileset.FirstGId + int.Parse(element.GetAttribute("id"));

			foreach (XmlElement subElement in element.ChildNodes)
			{
				if (subElement.Name == "properties")
				{
					ParseProperties(subElement, dict);
				}
			}

			TileProperties.Add(gid, dict);
		}

		private void ParseLayer(XmlElement element)
		{
			PKTmxLayerInfo layer = new PKTmxLayerInfo();
			layer.Name = element.GetAttribute("name");

			Size s = Size.Empty;
			s.Width = int.Parse(element.GetAttribute("width"));
			s.Height = int.Parse(element.GetAttribute("height"));
			layer.LayerSize = s;

			if (!element.HasAttribute("visible") || element.GetAttribute("visible") != "0")
			{
				layer.IsVisible = true;
			}
			else
			{
				layer.IsVisible = false;
			}

			if (!element.HasAttribute("opacity"))
			{
				layer.Opacity = 256;
			}
			else
			{
				layer.Opacity = (int)(256 * float.Parse(element.GetAttribute("opacity")));
			}

			int x, y;
			if (int.TryParse(element.GetAttribute("x"), out x) && int.TryParse(element.GetAttribute("y"), out y))
			{
				layer.Offset = new Point(x, y);
			}

			foreach (XmlElement subElement in element.ChildNodes)
			{
				if (subElement.Name == "properties")
				{
					ParseProperties(subElement, layer.Properties);
				}
				else if (subElement.Name == "data")
				{
					ParseData(subElement, layer);
				}
			}

			Layers.Add(layer);
		}

		private static void ParseData(XmlElement element, PKTmxLayerInfo layer)
		{
			string encoding = element.GetAttribute("encoding");
			string compression = element.GetAttribute("compression");
			if (encoding == "base64")
			{
				if (compression == "gzip")
				{
					DecodeData(element.InnerText.Trim(), true, false, layer);
				}
				else if (compression == "zlib")
				{
					DecodeData(element.InnerText.Trim(), false, true, layer);
				}
				else if (string.IsNullOrEmpty(compression))
				{
					DecodeData(element.InnerText.Trim(), false, false, layer);
				}
				else
				{
					throw new NotSupportedException("PKTmxMapInfo.ParseData: Unsupported TMX compression method: " + compression);
				}
			}
			else
			{
				throw new NotSupportedException("PKTmxMapInfo.ParseData: Unsupported TMX data encoding: " + encoding);
			}
		}

		private static void DecodeData(string data, bool isGzipCompressed, bool isZlibCompressed, PKTmxLayerInfo layer)
		{
			byte[] buffer = Convert.FromBase64String(data);
			Debug.Assert(buffer != null, "PKTmxMapInfo.DecodeData: Cannot decode TMX data.");

			if (isGzipCompressed || isZlibCompressed)
			{
				byte[] deflated = null;
				Size s = layer.LayerSize;
				int sizeHint = (int)(s.Width * s.Height * 4);

				int inflatedLen;
				if (isGzipCompressed)
				{
					inflatedLen = ZipUtils.InflateMemoryWithHint(buffer, out deflated, sizeHint);
				}
				else
				{
					inflatedLen = ZipUtils.InflateZlibMemoryWithHint(buffer, out deflated, sizeHint);
				}
				Debug.Assert(deflated != null, "PKTmxMapInfo.DecodeData: Cannot inflate TMX data.");
				Debug.Assert(inflatedLen == sizeHint, "PKTmxMapInfo.DecodeData: Inflated length does not match layer size.");

				int length = deflated.Length / 4;
				layer.Tiles = new int[length];
				for (int i = 0; i < length; i++)
				{
					layer.Tiles[i] = BitConverter.ToInt32(deflated, i * 4);
				}
			}
			else
			{
				int length = buffer.Length / 4;
				layer.Tiles = new int[length];
				for (int i = 0; i < length; i++)
				{
					layer.Tiles[i] = BitConverter.ToInt32(buffer, i * 4);
				}
			}
		}

		private void ParseObjectGroup(XmlElement element)
		{
			PKTmxObjectGroup objectGroup = new PKTmxObjectGroup();
			objectGroup.GroupName = element.GetAttribute("name");

			int x, y;
			if (int.TryParse(element.GetAttribute("x"), out x) && int.TryParse(element.GetAttribute("Y"), out y))
			{
				x *= TileSize.Width;
				y *= TileSize.Height;
				objectGroup.PositionOffset = new Point(x, y);
			}

			foreach (XmlElement subElement in element.ChildNodes)
			{
				if (subElement.Name == "object")
				{
					ParseObject(subElement, objectGroup);
				}
				else if (subElement.Name == "properties")
				{
					ParseProperties(subElement, objectGroup.Properties);
				}
			}

			ObjectGroups.Add(objectGroup);
		}

		private static void ParseObject(XmlElement element, PKTmxObjectGroup parentObjectGroup)
		{
			PKTmxObject obj = new PKTmxObject();
			obj.ObjectName = element.GetAttribute("name");

			if (element.HasAttribute("x") && element.HasAttribute("y") && element.HasAttribute("width") && element.HasAttribute("height"))
			{
				int x = int.Parse(element.GetAttribute("x")) + parentObjectGroup.PositionOffset.X;
				int y = int.Parse(element.GetAttribute("y")) + parentObjectGroup.PositionOffset.Y;
				int width = int.Parse(element.GetAttribute("width"));
				int height = int.Parse(element.GetAttribute("height"));
				obj.Rect = new Rectangle(x, y, width, height);
			}
			else
			{
				throw new NotSupportedException("PKMapInfo.ParseObject: Only support rectangle object for object group.");
			}

			foreach (XmlElement subElement in element.ChildNodes)
			{
				if (subElement.Name == "properties")
				{
					ParseProperties(subElement, obj.Properties);
				}
			}
		}

		private static void ParseProperties(XmlElement element, Dictionary<string, string> dict)
		{
			foreach (XmlElement subElement in element.ChildNodes)
			{
				string pName = subElement.GetAttribute("name");
				string pValue = subElement.GetAttribute("value");
				dict.Add(pName, pValue);
			}
		}

		private string GetFileFullPath(string relativePath)
		{
			string fullPath;
			if (!string.IsNullOrEmpty(FileName))
			{
				fullPath = FileName.Substring(0, FileName.LastIndexOf('\\'));
			}
			else
			{
				fullPath = Resources;
			}
			while (relativePath.Substring(0, 2) == "..")
			{
				fullPath = fullPath.Substring(0, fullPath.LastIndexOf('\\'));
				relativePath = relativePath.Substring(3);
			}
			if (relativePath.Substring(0, 1) == ".")
			{
				relativePath = relativePath.Substring(2);
			}
			return Path.Combine(fullPath, relativePath);
		}
		#endregion
	}
}
